import { callStack } from '@/runner/callStack'
import ActionTypes from './action-types'
import { MacroStatus } from '@/types/macro'
import {
  pick,
  on,
  map,
  compose,
  uid,
  loadCsv,
  delay,
  loadImage
} from '@/common/utils'
import {
  getBreakpoints,
  getBreakpointsByMacroId,
  getCurrentMacroId,
  getErrorCommandIndices,
  getWarningCommandIndices,
  getMacroFileNodeList,
  hasUnsavedMacro
} from '../recomputed'
import { State, RunBy } from '@/reducers'
import { getStorageManager, StorageManagerEvent, StorageTarget } from '@/file_storage'
import log from '@/common/log'
import { panelUpdateBadge } from '@/ext/badge'
import { isValidCmd } from '@/common/command'
import { normalizeCommand, normalizeTestCase } from '@/common/test_case_model'
import { getMiscData, MiscKey } from '../services/kv_data/misc_data'
import { askBg } from '@/ext/ipc'
import { uniqueName, sanitizeFileName, arrayBufferToString } from '@/common/utils';
import { message } from 'antd'
import preTcs from '../config/preinstall_macros'
import config from '../config'
import { uniqueStrings, flow } from '../common/ts_utils'
import storage from '../common/ext_storage_local'
import { fromJSONString } from '../common/convert_utils'
import { Dispatch, UnknownAction } from 'redux'

let recordedCount: number = 0

const toLower = (str: string) => (str || '').toLowerCase()

export const findSameNameMacro = (name: string, macros: any[]) => {
  return macros.find(tc => toLower(tc.name) === toLower(name))
}

export const findSamePathMacro = (path: string, macroNodes: any[]) => {
  const converPath = (str: string) => toLower(str).replace(/.json$/, '')
  return macroNodes.find(node => converPath(node.relativePath) === converPath(path))
}

export const startRecording = () => {
  return {
    type: ActionTypes.START_RECORDING_SUCCESS
  }
}

export const stopRecording = () => {
  return {
    type: ActionTypes.STOP_RECORDING_SUCCESS
  }
}
// 开始捕获
export const startInspecting = () => async (dispatch, getState) => {
  dispatch({
    type: ActionTypes.START_INSPECTING,
  })
  await askBg('PANEL_START_INSPECTING', {})
  dispatch({
    type: ActionTypes.START_INSPECTING_SUCCESS,
  })
  // return {
  //   type: ActionTypes.START_INSPECTING,
  //   promise: () => {
  //     return askBg('PANEL_START_INSPECTING', {})
  //   }
  // }
}

export const stopInspecting = () => async (dispatch, getState) => {
  dispatch({
    type: ActionTypes.STOP_INSPECTING,
  })
  await askBg('PANEL_STOP_INSPECTING', {})
  dispatch({
    type: ActionTypes.STOP_INSPECTING_SUCCESS,
  })
  // return {
  //   type: ActionTypes.STOP_INSPECTING,
  //   promise: () => {
  //     return askBg('PANEL_STOP_INSPECTING', {})
  //   }
  // }
}

export const startPlaying = () => {
  return {
    type: ActionTypes.START_PLAYING,
    data: null
  }
}

export const stopPlaying = () => {
  return {
    type: ActionTypes.STOP_PLAYING,
    data: null
  }
}

export const doneInspecting = () => {
  return {
    type: ActionTypes.DONE_INSPECTING,
    data: null
  }
}

export const increaseRecordedCount = () => {
  recordedCount += 1
  // Note: show in badge the recorded count
  askBg('PANEL_UPDATE_BADGE', {
    type: 'record',
    text: '' + recordedCount
  })
}

export const appendCommand = (cmdObj: any, fromRecord: boolean) => {
  if (fromRecord) {
    increaseRecordedCount()
  }

  return {
    type: ActionTypes.APPEND_COMMAND,
    data: { command: normalizeCommand(cmdObj) },
  }

}

export const duplicateCommand = (index: number) => {
  return {
    type: ActionTypes.DUPLICATE_COMMAND,
    data: { index }
  }
}

export const insertCommand = (cmdObj: any, index: number, fromRecord = false) => {
  if (fromRecord) {
    increaseRecordedCount()
  }

  return {
    type: ActionTypes.INSERT_COMMAND,
    data: {
      index,
      command: normalizeCommand(cmdObj)
    }
  }
}

export const updateCommand = (cmdObj: any, index: number) => {
  return {
    type: ActionTypes.UPDATE_COMMAND,
    data: {
      command: normalizeCommand(cmdObj),
      index: index
    }
  }
}

export const removeCommand = (index: number) => {
  return {
    type: ActionTypes.REMOVE_COMMAND,
    data: { index }
  }
}

export const selectCommand = (index: number, forceClick: boolean) => {
  return {
    type: ActionTypes.SELECT_COMMAND,
    data: { index, forceClick },
  }
}

export const updateEditing = (editing: any) => {
  return {
    type: ActionTypes.UPDATE_EDITING,
    data: { editing },
  }
}

export const cutCommand = (index: number) => {
  return {
    type: ActionTypes.CUT_COMMAND,
    data: { indices: [index] }
  }
}

export const copyCommand = (index: number) => {
  return {
    type: ActionTypes.COPY_COMMAND,
    data: { indices: [index] }
  }
}

export const pasteCommand = (index: number) => {
  return {
    type: ActionTypes.PASTE_COMMAND,
    data: { index },
  }
}

export const normalizeCommands = () => {
  return {
    type: ActionTypes.NORMALIZE_COMMANDS,
    data: {}
  }
}

export const updateSelectedCommand = (obj: any) => {
  return {
    type: ActionTypes.UPDATE_SELECTED_COMMAND,
    data: obj
  }
}

export const toggleComment = (commandIndex: number) => {
  return (dispatch: Dispatch<UnknownAction>, getState: () => State) => {
    const state = getState()
    const commands = state.editor.editing.commands
    const command = commands[commandIndex]

    if (!command || !command.cmd || !command.cmd.length) {
      return
    }

    // Note: for commented out command, its data looks like:
    // {
    //   cmd:     'comment',
    //   target:  'originalCmd // originalTarget
    //   value:   not touched
    // }
    if (command.cmd === 'comment') {
      const separator = ' // '
      const index = command.target.indexOf(separator)
      if (index === -1) return

      const cmd = command.target.substr(0, index)
      if (!isValidCmd(cmd)) return

      const target = command.target.substr(index + separator.length)

      return dispatch(updateCommand({
        ...command,
        cmd,
        target
      }, commandIndex))
    } else {
      return dispatch(updateCommand({
        ...command,
        cmd: 'comment',
        target: `${command.cmd} // ${command.target || ''}`
      }, commandIndex))
    }
  }
}

export const toggleCommentOnSelectedCommand = () => {
  return (dispatch: Dispatch<any>, getState: () => State) => {
    const state = getState()
    const index = state.editor.editing.meta.selectedIndex

    dispatch(toggleComment(index))
  }
}

export const setSourceError = (error: any) => {
  return {
    type: ActionTypes.SET_SOURCE_ERROR,
    data: error
  }
}

export const setSourceCurrent = (str: string) => {
  return {
    type: ActionTypes.SET_SOURCE_CURRENT,
    data: str
  }
}

// 根据源码视图更新列表视图
export const saveSourceCodeToEditing = (str: string) => {
  return (dispatch: Dispatch<UnknownAction>, getState: () => State) => {
    const { editing, editingSource } = getState().editor
    if (editingSource.pure === editingSource.current) return

    // log('ACTION, saveSourceCodeToEditing', str)

    try {
      const obj = fromJSONString(str, 'untitled')

      dispatch(setEditing({
        ...obj.data,
        meta: editing.meta
      }))

      dispatch(setSourceError(null))
    } catch (e: any) {
      message.error('There are errors in the source')
      dispatch(setSourceError(e.message))
    }
  }
}

export const saveEditingAsExisted = () => {
  return async (dispatch: Dispatch<any>, getState: () => State) => {

    const state = getState()
    const src = state.editor.editing.meta.src
    const macroId = src!.id
    const data = pick(['commands'], state.editor.editing)
    const macroStorage = getStorageManager().getMacroStorage()
    if (!macroId) {
      throw new Error(`Can't find macro with path '${macroId}'`)
    }
    // Make sure, only 'cmd', 'value', 'target' are saved in storage
    data.commands = data.commands.map(normalizeCommand)
    if (hasUnsavedMacro(state)) {
      // Reset test case status
      dispatch(updateMacroPlayStatus(macroId, null))
    }
    const macro = macroStorage.read(macroId, 'Text')
    const updatedMacro = { ...macro, data }
    dispatch({
      type: ActionTypes.SET_CURRENT_MACRO,
      data: updatedMacro
    })
    await macroStorage.write(macroId, updatedMacro)
    dispatch({
      type: ActionTypes.SAVE_EDITING_AS_EXISTED,
      data: null
    })
  }
}

// In the form of redux-thunnk, it saves the current editing test case as a new named test case
export const saveEditingAsNew = (name: string) => {
  return async (dispatch: Dispatch<UnknownAction>, getState: () => State) => {
    const state = getState()
    const data = pick(['commands'], state.editor.editing)
    const sameName = findSameNameMacro(name, state.editor.testCases)

    if (sameName) {
      return Promise.reject(new Error('The macro name already exists!'))
    }

    const relativePath = '/' + name + '.json'
    const id = getStorageManager().getMacroStorage().filePath(relativePath)
    const newMacro = { id, name, data }

    await getStorageManager().getMacroStorage().write(relativePath, newMacro)

    dispatch({
      type: 'setCurrentMacro',
      data: newMacro
    })

    dispatch({
      type: ActionTypes.SAVE_EDITING_AS_NEW,
      data: {
        id,
        name
      },
    })

  }
}

export const resetEditing = () => {
  return (dispatch: Dispatch<any>, getState: () => State) => {
    const state = getState()
    const { editing } = state.editor
    const macroNodes = getMacroFileNodeList(state)

    // Leave it if it's a new macro
    if (editing.meta && !editing.meta.src) return
    if (macroNodes.length === 0) {
      dispatch(editNewTestCase())
    } else {
      dispatch(editTestCase(macroNodes[0].fullPath))
    }
  }
}

export const resetEditingIfNeeded = () => {
  return (dispatch: Dispatch<any>, getState: () => State) => {
    const state = getState()
    const { editing } = state.editor
    const lastTcId = editing.meta.src && editing.meta.src.id

    if (!lastTcId) {
      return resetEditing()(dispatch, getState)
    } else {
      dispatch(editTestCase(lastTcId))
    }
  }
}

export const setEditing = (editing: Record<string, any>) => {
  return {
    type: ActionTypes.SET_EDITING,
    data: editing
  }
}

export const editTestCase = (id: string) => {
  return (dispatch: Dispatch<UnknownAction>, getState: () => State) => {
    return getStorageManager().getMacroStorage().read(id, 'Text')
      .then(rawMacro => {
        const macro = normalizeTestCase(rawMacro)

        dispatch({
          type: ActionTypes.SET_CURRENT_MACRO,
          data: macro
        })

        dispatch({
          type: ActionTypes.EDIT_TEST_CASE,
          data: {
            id,
            macro
          },

        })

        // Save last edited macro id for each mode,
        // so that we can recover to it after switching mode

        getMiscData().set(MiscKey.BrowserModeLastMacroId, id)

        return macro
      })
  }
}

export const editNewTestCase = () => {
  return {
    type: ActionTypes.EDIT_NEW_TEST_CASE,
    data: null,
  }
}

export const upsertTestCase = (tc: any) => {
  return (dispatch: Dispatch<UnknownAction>, getState: () => State) => {
    return getStorageManager()
      .getMacroStorage()
      .write(tc.name, {
        id: uid(),
        ...tc
      })
  }
}

// 删除用例
export const removeTestCase = (macroId: string) => {
  return async (dispatch: Dispatch<any>, getState: () => State) => {
    const state = getState()
    const curId = state.editor.editing.meta.src && state.editor.editing.meta.src.id
    const tss = state.editor.testSuites.filter(ts => ts.cases.find(m => m.testCaseId === macroId))

    if (tss.length > 0) {
      return Promise.reject(new Error(`Can't delete this macro for now, it's currently used in following test suites: ${tss.map(item => item.name)}`))
    }

    // Reset test case status
    dispatch(updateMacroPlayStatus(macroId, null))

    await getStorageManager().getMacroStorage().remove(macroId)

    dispatch(getMacroFolderStructure())
    dispatch({
      type: ActionTypes.REMOVE_TEST_CASE,
      data: {
        isCurrent: curId === macroId
      },
    })

  }
}

export const setPlayerState = (obj: Record<string, any>) => {
  return {
    type: ActionTypes.SET_PLAYER_STATE,
    data: obj
  }
}

export const setTimeoutStatus = (args: any) => {
  return (dispatch: Dispatch<UnknownAction>) => {
    dispatch(setPlayerState({
      timeoutStatus: args
    }))

    panelUpdateBadge({
      type: 'play',
      text: (args.total - args.past) / 1000 + 's'
    })

  }
}

// 增加警告信息
export const addPlayerWarningCommandIndex = (index: number) => {
  return (dispatch: Dispatch<UnknownAction>, getState: () => State) => {
    const state = getState()
    const macroId = getCurrentMacroId(state)
    const indices = getWarningCommandIndices(state)
    const warningCommandIndices = indices.indexOf(index) === -1
      ? [...indices, index]
      : indices
    dispatch(updateMacroExtra(macroId, { warningCommandIndices }))
  }
}

// 增加出错信息
export const addPlayerErrorCommandIndex = (index: number) => {
  return (dispatch: Dispatch<UnknownAction>, getState: () => State) => {
    const state = getState()
    const macroId = getCurrentMacroId(state)
    const ids = getErrorCommandIndices(state)
    const errorCommandIndices = ids.indexOf(index) === -1
      ? [...ids, index]
      : ids
    dispatch(
      updateMacroExtra(macroId, { errorCommandIndices })
    )
  }
}

export const addLog = (type: string, text: string, options: any = {}) => {
  return (dispatch: Dispatch<UnknownAction>, getState: () => State) => {
    const state = getState()
    const stack = options.noStack ? [] : callStack.toArray()
    const logItem = {
      type,
      text,
      options,
      id: uid(),
      createTime: new Date(),
      stack: stack.map((item, i) => ({
        macroId: item.resource.id,
        macroName: item.resource.name,
        commandIndex: i === stack.length - 1 ? state.player.nextCommandIndex : item.runningStatus.nextIndex,
        isSubroutine: i !== 0
      }))
    }

    return dispatch({
      type: ActionTypes.ADD_LOGS,
      data: [logItem]
    })
  }
}

export const clearLogs = () => {
  return {
    type: ActionTypes.CLEAR_LOGS,
    data: null
  }
}

export const addScreenshot = (screenshot: any) => {
  return {
    type: ActionTypes.ADD_SCREENSHOT,
    data: {
      ...screenshot,
      createTime: new Date()
    }
  }
}

export const clearScreenshots = () => async (dispatch: Dispatch<UnknownAction>) => {
  await getStorageManager().getScreenshotStorage().clear()
  dispatch({
    type: ActionTypes.CLEAR_SCREENSHOTS,
    data: null
  })
}

export const addVision = (vision: any) => {
  return {
    type: ActionTypes.ADD_VISION,
    data: {
      ...vision,
      createTime: new Date()
    }
  }
}

export const clearVisions = () => {
  return {
    type: ActionTypes.CLEAR_VISIONS,
    data: null,
    post: () => {
      return getStorageManager()
        .getVisionStorage()
        .clear()
    }
  }
}

export const updateConfig = (data: any) => {
  return {
    type: ActionTypes.UPDATE_CONFIG,
    data: data,
  }
}

export const setMacrosExtra = (macrosExtra: Record<string, any>) => {
  return {
    type: ActionTypes.SET_MACROS_EXTRA,
    data: macrosExtra
  }
}

// 更新额外信息
export const updateMacroExtra = (id: string, extra: Record<string, any>) => {
  // TODO: the key for extra info should be different,
  // something like storage mode + storage path + file name
  return {
    type: ActionTypes.UPDATE_ONE_MACRO_EXTRA,
    data: { id, extra }
  }
}

export const updateMacroPlayStatus = (id: string, status: any) => {
  return (dispatch: Dispatch<UnknownAction>) => {
    dispatch(updateMacroExtra(id, { status }))
  }
}

// 更新断点
export const updateMacroBreakpoints = (id: string, breakpointIndices: number[]) => {
  return (dispatch: Dispatch<UnknownAction>) => {
    dispatch(updateMacroExtra(id, { breakpointIndices }))
  }
}

// 更新完成信息
export const updateMacroDoneCommandsIndices = (id: string, doneCommandIndices: number[]) => {
  return (dispatch: Dispatch<UnknownAction>) => {
    dispatch(updateMacroExtra(id, { doneCommandIndices }))
  }
}

export const updateProxy = (proxy: string) => {
  return {
    type: ActionTypes.UPDATE_PROXY,
    data: proxy
  }
}

export const commonPlayerState = (state: Record<string, any>, options: Record<string, any>, macroId: string, macroName: string) => {
  const { config } = state
  const cfg = pick(['playHighlightElements', 'playScrollElementsIntoView'], config)
  const finalMacroName = (() => {
    if (macroName) {
      return macroName
    }

    if (!macroId) {
      return state.editor.editing.meta.src ? state.editor.editing.meta.src.name : 'Untitled'
    }

    const macro = getMacroFileNodeList(state).find(node => node.fullPath === macroId)

    if (!macro) {
      throw new Error(`can't find macro with id '${macroId}'`)
    }

    return macro.name
  })()
  const scope = {
    '!MACRONAME': finalMacroName,
    '!TIMEOUT_PAGELOAD': parseFloat(config.timeoutPageLoad),
    '!TIMEOUT_WAIT': parseFloat(config.timeoutElement),
    '!TIMEOUT_MACRO': parseFloat(config.timeoutMacro),
    '!TIMEOUT_DOWNLOAD': parseFloat(config.timeoutDownload),
    '!OCRLANGUAGE': config.ocrLanguage,
    '!CVSCOPE': config.cvScope,
    '!REPLAYSPEED': ({
      '0': 'FAST',
      '0.3': 'MEDIUM',
      '2': 'SLOW'
    })[options.postDelay / 1000] || 'MEDIUM',
    ...(options.overrideScope || {})
  }

  const breakpoints = macroId ? getBreakpointsByMacroId(state, macroId) : getBreakpoints(state)

  const opts = compose(
    on('resources'),
    map,
    on('extra')
  )((extra = {}) => ({
    ...extra,
    ...cfg,
    ...(options.commandExtra || {})
  }))(options)

  const playerState = {
    title: finalMacroName,
    ...opts,
    public: {
      ...(opts.public || {}),
      scope
    },
    breakpoints: [...breakpoints, ...(options.breakpoints || [])]
  }

  return playerState
}

export const playerPlay = (options: Record<string, any>): any => {
  // Filter out empty commands
  const opts: Record<string, any> = {
    ...options,
    resources: (options.resources || []).filter((res: any) => res.cmd && res.cmd.length > 0)
  }

  return (dispatch: Dispatch<UnknownAction>, getState: () => State) => {
    const state = getState()
    const playerState = commonPlayerState(state, opts, opts.macroId, opts.title)

    callStack.clear()

    return callStack.call({
      id: opts.macroId,
      name: playerState.title,
      commands: opts.resources,
      path: ''
    }, {
      playerState,
      status: MacroStatus.Running,
      nextIndex: opts.startIndex,
      commandResults: []
    })
  }
}

export const listCSV = () => {
  return (dispatch: Dispatch<UnknownAction>) => {
    const csvStorage = getStorageManager().getCSVStorage()

    csvStorage.list()
      .then(list => {
        return Promise.all(list.map(item => {
          return {
            name: item.name,
            size: item.size,
            fullPath: item.fullPath,
            createTime: new Date(item.lastModified)
          }
        }))
      })
      .then(list => {
        dispatch({
          type: ActionTypes.SET_CSV_LIST,
          data: list
        })
      })
      .catch(e => {
        log.error('listCSV error', e)
        return Promise.reject(e)
      })
  }
}

export const listScreenshots = () => {
  return (dispatch: Dispatch<UnknownAction>) => {
    const man = getStorageManager().getScreenshotStorage()

    return man.list().then(list => {
      // log('listScreenshots', list)

      return list.map(item => ({
        name: item.name,
        fullPath: item.fullPath,
        createTime: new Date(item.lastModified)
      }))
    })
      .then(list => {
        dispatch({
          type: ActionTypes.SET_SCREENSHOT_LIST,
          data: list
        })
      })
      .catch(e => {
        log.error('listScreenshots error', e)
        return Promise.reject(e)
      })
  }
}

export const listVisions = () => {
  return (dispatch: Dispatch<UnknownAction>) => {
    const visionStorage = getStorageManager().getVisionStorage()

    return visionStorage.list().then(list => {
      // log('listVisions', list)

      return list.map(item => ({
        name: item.name,
        fullPath: item.fullPath,
        createTime: new Date(item.lastModified)
      }))
    })
      .then(list => {
        dispatch({
          type: ActionTypes.SET_VISION_LIST,
          data: list
        })
      })
      .catch(e => {
        log.error('listVisions error', e)
        return Promise.reject(e)
      })
  }
}

export const renameVisionImage = (fileName: string, shouldUpdateCommand = true) => {
  return (dispatch: Dispatch<UnknownAction>, getState: () => State) => {
    const state = getState()
    const visionStorage = getStorageManager().getVisionStorage()
    const vision = visionStorage.read(fileName, 'Text')
  }
}

export const setVariables = (variables: Record<string, any>[]) => {
  variables.sort((a, b) => {
    if (a.key < b.key) return -1
    if (a.key > b.key) return 1
    return 0
  })

  return {
    type: ActionTypes.SET_VARIABLE_LIST,
    data: variables
  }
}
// 更新UI
export const updateUI = (data: any) => {
  return {
    type: ActionTypes.UPDATE_UI,
    data
  }
}

// 添加断点
export const addBreakpoint = (macroId: string, commandIndex: number) => {
  return (dispatch: Dispatch<any>, getState: () => State) => {
    const state = getState()
    const extra: any = state.editor.macrosExtra[macroId] || {}
    const indices: number[] = extra.breakpointIndices || []
    const newIndices = indices.indexOf(commandIndex) === -1 ? [...indices, commandIndex] : indices

    dispatch(updateMacroBreakpoints(macroId, newIndices))
  }
}

// 删除断点
export const removeBreakpoint = (macroId: string, commandIndex: number) => {
  return (dispatch: Dispatch<any>, getState: () => State) => {
    const state = getState()
    const extra: any = state.editor.macrosExtra[macroId] || {}
    const indices: number[] = extra.breakpointIndices || []
    const newIndices = indices.filter(index => index !== commandIndex)

    dispatch(updateMacroBreakpoints(macroId, newIndices))
  }
}

export const setEditorActiveTab = (tab: string) => {
  return {
    type: ActionTypes.SET_EDITOR_ACTIVE_TAB,
    data: tab
  }
}

export const preinstall = (yesInstall = true) => {
  return (dispatch: Dispatch<any>, getState: () => State) => {
    const markThisVersion = () => {
      return storage.get('preinstall_info')
        .then((info = {}) => {
          const prevVersions = info.askedVersions || []
          const thisVersion = config.preinstall.version
          const hasThisOne = prevVersions.indexOf(thisVersion) !== -1

          if (hasThisOne) return true

          return storage.set('preinstall_info', {
            ...info,
            askedVersions: [...prevVersions, thisVersion]
          })
        })
    }

    if (!yesInstall) return markThisVersion()

    log('PREINSTALL_CSV_LIST', (window as any).PREINSTALL_CSV_LIST)
    log('PREINSTALL_VISION_LIST', (window as any).PREINSTALL_VISION_LIST)

    const installMacrosAndSuites = () => {
      if (!preTcs || !Object.keys(preTcs).length) return Promise.resolve()

      const installMacros = () => {
        const macroStorage = getStorageManager().getMacroStorage()
        const path = macroStorage.getPathLib()
        const folders = Object.keys(preTcs).map(relativePath => {
          return path.join(
            config.preinstall.macroFolder,
            path.dirname(relativePath)
          )
        })
        const uniqueFolders = uniqueStrings(...folders)

        return flow(
          ...uniqueFolders.map(dirPath => () => macroStorage.ensureDirectory(dirPath))
        )
          .then(() => {
            return Promise.all(
              Object.keys(preTcs).map(relativePath => {
                const macroName = path.basename(relativePath)
                const filePath = macroStorage.filePath(
                  path.join(
                    config.preinstall.macroFolder,
                    relativePath
                  )
                )
                const str = JSON.stringify((preTcs as any)[relativePath])
                const macro = fromJSONString(str, macroName)

                return macroStorage.write(filePath, macro)
              })
            )
          })
      }

      return flow(installMacros)
    }

    // Preinstall csv
    const installCsvs = () => {
      const list = (window as any).PREINSTALL_CSV_LIST
      if (list.length === 0) return Promise.resolve()

      // Note: preinstalled resources all go into browser mode
      const csvStorage = getStorageManager().getStorageForTarget(StorageTarget.CSV)

      return csvStorage.ensureDir()
        .then(() => {
          const ps = list.map((url: string) => {
            const parts = url.split('/')
            const fileName = parts[parts.length - 1]

            return loadCsv(url)
              .then(text => {
                return csvStorage.write(fileName, new Blob([text]))
              })
          })

          return Promise.resolve(ps)
            // Note: delay needed for Firefox and slow Chrome
            .then(() => delay(() => { }, 3000))
            .then(() => {
              dispatch(listCSV())
            })
        })
    }

    // Preinstall vision images
    const installVisionImages = () => {
      const list = (window as any).PREINSTALL_VISION_LIST
      if (list.length === 0) return Promise.resolve()

      // Note: preinstalled resources all go into browser mode
      const visionStorage = getStorageManager().getStorageForTarget(StorageTarget.Vision)

      return visionStorage.ensureDir()
        .then(() => {
          const ps = list.map((url: string) => {
            const parts = url.split('/')
            const fileName = parts[parts.length - 1]

            return loadImage(url)
              .then(blob => {
                return visionStorage.write(fileName, blob)
              })
          })

          return Promise.resolve(ps)
            // Note: delay needed for Firefox and slow Chrome
            .then(() => delay(() => { }, 3000))
            .then(() => {
              dispatch(listVisions())
            })
        })
    }

    return Promise.all([
      installMacrosAndSuites(),
      installCsvs(),
      installVisionImages()
    ])
      .then(markThisVersion)
  }
}


// simple_actions
export const setNoDisplayInPlay = (data: boolean) => {
  return {
    type: 'SET_NO_DISPLAY_IN_PLAY',
    data
  }
}

export const setOcrInDesktopMode = (ocrInDesktopMode: boolean) => {
  return {
    type: ActionTypes.SET_OCR_IN_DESKTOP_MODE,
    data: ocrInDesktopMode
  }
}

export const setFrom = (from: RunBy) => (dispatch: Dispatch<UnknownAction>, getState: () => State) => {
  const { from: oldFrom } = getState()

  if (from === RunBy.Manual && oldFrom !== RunBy.Manual) {
    getStorageManager().emit(StorageManagerEvent.ForceReload)
  }

  return dispatch({
    type: ActionTypes.SET_FROM,
    data: from
  })
}

export const setIsLoadingMacros = (isLoading: boolean) => {
  return {
    type: ActionTypes.SET_IS_LOADING_MACROS,
    data: isLoading
  }
}

export const setMacroFolderStructure = (entryNodes: Record<string, any>) => {
  return {
    type: ActionTypes.SET_MACRO_FOLDER_STRUCTURE,
    data: entryNodes
  }
}

export const getMacroFolderStructure = () => {
  return async (dispatch: Dispatch<UnknownAction>) => {
    const list = await getStorageManager().getMacroStorage().listR()
    dispatch(setMacroFolderStructure(list))
  }
}

export const toggleRecorderSkipOpen = (skipOpen: boolean) => {
  return {
    type: ActionTypes.TOGGLE_RECORDER_SKIP_OPEN,
    data: skipOpen
  }
}

// 文件夹重命名
export const macroRenameFolder = (options: { dir: string; newName: string }) => {
  return async (dispatch: Dispatch<any>, getState: () => State) => {

    const { dir, newName } = options;
    const macroStorage = getStorageManager().getMacroStorage();
    const path = macroStorage.getPathLib();
    const dirFullPath = macroStorage.dirPath(dir);
    const folderName = path.basename(dirFullPath);
    const parentDir = path.dirname(dirFullPath);
    const editingId = getState().editor.editing.meta.src!.id;
    const isEditingCur = editingId.indexOf(dirFullPath + path.sep) === 0;
    const newFullPath = path.join(parentDir, sanitizeFileName(newName));
    const exists = await macroStorage.directoryExists(newFullPath);
    if (exists) {
      const msg = `'${newName}' already exists`
      message.error(msg)
      throw new Error(msg)
    }
    try {
      await macroStorage.moveDirectory(dirFullPath, newFullPath)
      message.success(`Renamed to '${newName}'`)
      dispatch(getMacroFolderStructure())
      if (isEditingCur) {
        const newMacroPath = editingId.replace(dirFullPath, newFullPath)
        dispatch(editTestCase(newMacroPath))
      }
    } catch (error: any) {
      log.error(error)
      const msg = 'Failed to rename: ' + error.message
      message.error(msg)
      throw new Error(msg)
    }
  }
}

// 用例重命名
export const renameTestCase = (newName: string, fullPath: string) => async (dispatch: Dispatch<any>, getState: () => State) => {
  const macroStorage = getStorageManager().getMacroStorage()
  const path = macroStorage.getPathLib()
  const state = getState()
  const editingId = state.editor.editing.meta.src!.id
  const newPath = macroStorage.filePath(path.join(path.dirname(fullPath), sanitizeFileName(newName)))
  const exists = await macroStorage.fileExists(fullPath)
  if (!exists) {
    throw new Error(`'${fullPath}'用例不存在!`)
  }
  const newExists = await macroStorage.fileExists(newPath)
  if (newExists) {
    throw new Error('用例名称已存在!')
  }

  await macroStorage.rename(fullPath, newPath)

  if (editingId === fullPath) {
    dispatch({
      type: ActionTypes.RENAME_TEST_CASE,
      data: newName,
    })
  }

  const isRenamingCurrentMacro = editingId === fullPath
  if (isRenamingCurrentMacro) {
    dispatch(editTestCase(newPath))
  }

  dispatch(getMacroFolderStructure())
}


// 删除文件夹
export const macroDeleteFolder = (options: { dir: string }) => {
  return async (dispatch: Dispatch<any>) => {
    const { dir } = options
    const macroStorage = getStorageManager().getMacroStorage()
    await macroStorage.remove(dir, true)
    dispatch(getMacroFolderStructure())
  }
}

// 移动命令
export const moveCommands = (startIndex: number, endIndex: number) => {
  return {
    type: ActionTypes.MOVE_COMMANDS,
    data: {
      startIndex,
      endIndex
    }
  }
}

export const setIndexToInsertRecorded = (data: number) => {
  return {
    type: ActionTypes.SET_INDEX_TO_INSERT_RECORDED,
    data
  }
}

// 移动用例目录
export const macroMoveEntry = (data: any) => async (dispatch: Dispatch<any>) => {
  const macroStorage = getStorageManager().getMacroStorage()
  await macroStorage.move(data.entryId, data.dirId, data.isSourceDirectory, true)
  dispatch(getMacroFolderStructure())
}